/*
Author: 摘星fy
Editor: SublimeText
  Date: 2023-11-26 23:53:33
    QQ: 273259755
    WX: y273259755
*/

export default class BrainGraph {
  constructor(d = {}) {
    this.d = d

    d.conf ??= {}
    d.conf.fontSize ??= 14
    d.conf.lineHeight ??= d.conf.fontSize * 1.5
    d.conf.fontFamily ??= 'Arial'
    d.conf.font = d.conf.fontSize + 'px ' + d.conf.fontFamily
    d.conf.scale ??= devicePixelRatio
    d.conf.rowSpace ??= 50
    d.conf.cellPaddingH ??= 8
    d.conf.cellPaddingV ??= 2
    d.conf.lineWidth ??= 1

    d.translate ??= {}
    d.translate.x ??= 0
    d.translate.y ??= 0

    d.color ??= {}
    d.color.curNode ??= '#673ab7'
    d.color.highlight ??= '#9c27b0'
    d.color.selected ??= '#2196f3'
    d.color.grey = '#9e9e9e'

    d.rec = {x: 0, y: 0, w: 0, h: 0}
    d.mapClose = {}
    d.mapSelected = {}
    d.isHighlight = false

    d.mapId = {}
    d.mapPid = {}
    d.roots = []
    d.stair = []

    this.setCanvas()
    this.setData()
  }
  setCanvas(canvas) {
    const d = this.d

    if (canvas instanceof HTMLCanvasElement) {
      d.canvas = canvas
      if (d.canvas.brainGraphed) {
        throw 'BrainGraph 已被初始化'
      }
      d.canvas.brainGraphed = true
    } else if (!(d.canvas instanceof HTMLCanvasElement)) {
      d.canvas = document.createElement('canvas')
    }

    d.canvas.tabIndex = 1
    d.canvas.style.outline = 'none'
    document.body.contains(d.canvas) && d.canvas.focus()
    d.w = d.canvas.offsetWidth
    d.h = d.canvas.offsetHeight
    d.canvas.width = d.w * d.conf.scale
    d.canvas.height = d.h * d.conf.scale
  }
  setData(data) {
    const d = this.d
    const gd = d.canvas.getContext('2d')

    const mapId = {}
    const mapPid = {}
    const roots = []
    const stair = []

    if (data instanceof Array) {
      d.data = data
    } else if (!(d.data instanceof Array)) {
      d.data = []
    }

    gd.font = d.conf.font

    d.data.forEach(v => {
      if (mapId[v.id]) {
        console.error('id repeat', v, mapId[v.id])
        throw ''
      }

      v.name_ ??= (v.name || v.id.toString()).trim().split('\n')
      v.w ??= Math.ceil(this.max(v.name_.map(str => gd.measureText(str).width)) + d.conf.cellPaddingH * 2)
      v.h ??= v.name_.length * d.conf.fontSize * 1.5 + d.conf.cellPaddingV * 2
      v.x = v.y = 0

      mapId[v.id] = v
      mapPid[v.pid] ??= []
      mapPid[v.pid].push(v)
    })

    const visited = {}
    d.data.forEach(v => {
      while (!visited[v.id]) {
        visited[v.id] = true
        const p = mapId[v.pid]
        if (!p) {
          roots.push(v)
          return
        }
        v = p
      }
    })

    const setDepth = (v, depth) => {
      stair[depth] ??= []
      v.depth = depth
      v.hIndex = stair[depth].length
      stair[depth].push(v)
      !d.mapClose[v.id] && mapPid[v.id]?.forEach(v_ => setDepth(v_, depth + 1))
    }
    roots.forEach(v => setDepth(v, 0))

    d.fibW = this.fib(stair.map(row => this.max(row.map(v => v.w))), d.conf.rowSpace)
    d.fibH = this.fib(stair.map(row => this.max(row.map(v => v.h))), d.conf.rowSpace)

    d.mapId = mapId
    d.mapPid = mapPid
    d.stair = stair
    d.roots = roots
    d.root = roots[0]
    
    this.setDirection(d.direction)
  }
  min(arr) {
    return Math.min.apply(null, arr)
  }
  max(arr) {
    return Math.max.apply(null, arr)
  }
  fib(arr, space) {
    const result = Array(arr.length)
    let t = 0
    for (let i = 0; i < arr.length; i++) {
      result[i] = t
      t += arr[i] + space
    }
    return result
  }
  setDirection(direction, offsetX = 15, offsetY = 15) {
    const d = this.d

    direction = direction ?? d.direction ?? 't'

    if (!'ltrb'.split('').includes(direction)) {
      throw 'setDirection error: ' + direction
    }

    d.direction = direction
    d.isH = 'tb'.indexOf(d.direction) > -1
    d.isL = d.direction === 'l'
    d.isT = d.direction === 't'

    if (!d.root) return

    switch (direction) {
      case 'l': d.translate = {x: offsetX, y: (d.h - d.root.h) / 2}; break
      case 't': d.translate = {x: (d.w - d.root.w) / 2, y: offsetY}; break
      case 'r': d.translate = {x: d.w - d.root.w - offsetX, y: (d.h - d.root.h) / 2}; break
      case 'b': d.translate = {x: (d.w - d.root.w) / 2, y: d.h - d.root.h - offsetY}; break
    }

    this.setTranslate(d.translate.x, d.translate.y)
  }
  setTranslate(x, y) {
    const d = this.d

    this.setLayout()
    d.translate.x = x
    d.translate.y = y

    const _x = x - d.root.x
    const _y = y - d.root.y

    d.roots.forEach((v) => {
      this.translate(v, _x, _y)
    })
    this.render()
  }
  setLayout() {
    const d = this.d
    const { mapPid, stair } = d
    const coord = d.isH ? 'x' : 'y'
    const coord_ = !d.isH ? 'x' : 'y'
    const attr = d.isH ? 'w' : 'h'
    const attr_ = !d.isH ? 'w' : 'h'

    for (let i = stair.length - 1; i > -1; i--) {
      const step = d[d.isH ? 'fibH' : 'fibW'][i]

      stair[i].forEach(v => {
        const children = this.getChildren(v)
        const prev = this.getPrev(v)

        v[coord_] = (d.isL || d.isT ? step : -step - v[attr_])

        if (!children.length) {
          v[coord] = prev ? prev[coord] + prev[attr] : 0
          return
        }

        const l = children[0]
        const r = children[children.length - 1]

        v[coord] = (l[coord] + r[coord] + r[attr]) / 2 - v[attr] / 2
        const dis = this.getDis(v)
        d.isH ? this.translate(v, -dis) : this.translate(v, 0, -dis)

        const leafNodes = []
        let t = prev
        while (t && t.pid === v.pid && !mapPid[t.id]?.length) {
          leafNodes.push(t)
          t = this.getPrev(t)
        }

        if (!leafNodes.length) return
        if (t && t.pid === v.pid) {
          const space = (v[coord] - t[coord] - t[attr] - leafNodes.reduce((t, v) => t += v[attr], 0)) / (leafNodes.length + 1)
          leafNodes.forEach(v => {
            const next = this.getNext(v)
            v[coord] = next[coord] - v[attr] - space
          })
        } else {
          leafNodes.forEach(v => {
            const next = this.getNext(v)
            v[coord] = next[coord] - v[attr]
          })
        }
      })
    }
  }
  uuid() {
    return '8-4-4-4-12'.replace(/\d+/g, (n) => {
      let t = ''
      n = Number(n)
      for (let i = 0; i < n; i++) {
        t += rand(0, 15).toString(16)
      }
      return t
    })
  }
  each(cb) {
    this.d.stair.forEach((row) => row.forEach(cb))
  }
  getPrev(v) {
    return this.d.stair[v.depth][v.hIndex - 1]
  }
  getNext(v) {
    return this.d.stair[v.depth][v.hIndex + 1]
  }
  getChildren(v) {
    const d = this.d
    return d.mapClose[v.id] || !d.mapPid[v.id]?.length ? [] : d.mapPid[v.id]
  }
  getAllChildren(v) {
    const result = []
    const dfs = v => {
      result.push(v)
      this.getChildren(v).forEach(dfs)
    }
    dfs(v)
    return result
  }
  getRoot(v) {
    while (v) {
      const t = this.d.mapId[v.pid]
      if (!t) return v
      v = t
    }
  }
  getBranchRoots() {
    const d = this.d
    if (d.curNode) return [d.curNode]

    const result = []
    const visited = {}

    Object.values(d.mapSelected).forEach(v => {
      while (!visited[v.id]) {
        visited[v.id] = true
        const p = d.mapId[v.pid]
        if (!d.mapSelected[p.id]) {
          result.push(v)
          return
        }
        v = p
      }
    })
    return result
  }
  translate(v, x = 0, y = 0) {
    if (!x && !y) return
    const dfs = v => {
      v.x += x
      v.y += y
      this.getChildren(v).forEach(dfs)
    }
    dfs(v)
  }
  contains(v, t) {
    const { mapId } = this.d
    while (t) {
      if (v === t) return true
      t = mapId[t.pid]
    }
    return false
  }
  getDis(v) {
    const d = this.d
    const coord = d.isH ? 'x' : 'y'
    const attr = d.isH ? 'w' : 'h'

    let dis = Infinity
    const dfs = v_ => {
      const prev = this.getPrev(v_)
      if (prev && !this.contains(v, prev)) {
        dis = Math.min(dis, v_[coord] - prev[coord] - prev[attr])
      }
      this.getChildren(v_).forEach(dfs)
    }
    dfs(v)
    return dis === Infinity ? 0 : dis
  }
  coll(a, b) {
    return !(
      a.x > b.x + b.w ||
      a.y > b.y + b.h ||
      b.x > a.x + a.w ||
      b.y > a.y + a.h
    )
  }
  pathHighlight(v) {
    this.d.isHighlight = true
    this.setCurNode(v)
  }
  pathHighlightSimple(v) {
    const d = this.d

    d.data.forEach((v) => {
      d.mapPid[v.id]?.length && (d.mapClose[v.id] = v)
    })

    let t = v
    while (t) {
      delete d.mapClose[t.id]
      t = d.mapId[t.pid]
    }

    this.setData()
    this.pathHighlight(v)
  }
  getHighlightNodes() {
    const d = this.d
    if (!d.isHighlight) return []
    const result = []
    let t = d.curNode
    while (t) {
      result.push(t)
      t = d.mapId[t.pid]
    }
    return result.reverse()
  }
  toggle(v) {
    const d = this.d
    if (!v) return
    d.mapClose[v.id] ? (delete d.mapClose[v.id]) : (d.mapClose[v.id] = v)
    const target = {x: v.x, y: v.y}
    this.setData()
    const dis = {
      x: target.x - v.x,
      y: target.y - v.y,
    }
    this.setTranslate(d.root.x + dis.x, d.root.y + dis.y)
  }
  openByDepth(maxDepth) {
    const d = this.d
    const dfs = (v, depth) => {
      depth > maxDepth - 2 && (d.mapClose[v.id] = v)
      d.mapPid[v.id]?.forEach(v_ => dfs(v_, depth + 1))
    }
    d.roots.forEach(v => dfs(v, 0))
    this.setData()
  }
  setCurNode(v) {
    const d = this.d
    d.mapSelected = {}
    d.curNode = v

    if (v) {
      let x = 0
      let y = 0

      if (v.x < 0) x = -v.x
      if (v.x + v.w > d.w) x = d.w - v.x - v.w

      if (v.y < 0) y = -v.y
      if (v.y + v.h > d.h) y = d.h - v.y - v.h
      
      ;(x || y) && d.roots.forEach(v => {
        this.translate(v, x, y)
      })
    }

    this.render()
  }
  logFormat(v) {
    const d = this.d
    const result = []
    const dfs = (v) => {
      result.push(v)
      this.getChildren(v).forEach(dfs)
    }
    v ? dfs(v) : d.roots.forEach(dfs)

    const str = result.map((v) => {
      return `${''.padEnd(v.depth, ' ')}${v.name}`
    })
    console.log(str.join('\n'))
  }
  catchBtnClose(e) {
    const d = this.d
    const {offsetX: x_, offsetY: y_} = e

    for (let i = d.stair.length - 1; i > -1; i--) {
      const row = d.stair[i]

      for (let j = row.length - 1; j > -1; j--) {
        const v = row[j]
        const children = d.mapPid[v.id]

        if (!children?.length) continue

        const r = 8
        let [x, y] = this.getPorts(children[0])

        switch (d.direction) {
          case 'l': x += r / 2 + 4; break
          case 't': y += r / 2 + 4; break
          case 'r': x -= r / 2 + 4; break
          case 'b': y -= r / 2 + 4; break
        }

        const a = x - x_
        const b = y - y_

        if (a * a + b * b < r * r) {
          return v
        }
      }
    }
  }
  catchNode(e, igor) {
    const d = this.d
    const visited = {}
    const {offsetX: x, offsetY: y} = e

    if (igor) {
      const dfs = v => {
        if (visited[v.id]) return
        visited[v.id] = true
        this.getChildren(v).forEach(dfs)
      }

      d.curNode ? (
        d.isHighlight
          ? dfs(this.getRoot(d.curNode))
          : dfs(d.curNode)
      ) : Object.values(d.mapSelected).forEach(dfs)
    }

    for (let i = d.stair.length - 1; i > -1; i--) {
      const row = d.stair[i]
      for (let j = row.length - 1; j > -1; j--) {
        const v = row[j]
        if (visited[v.id]) continue
        if (
          !(
            x < v.x ||
            y < v.y ||
            x > v.x + v.w ||
            y > v.y + v.h
          )
        ) return v
      }
    }
  }
  isNodeInScene(v) {
    const d = this.d
    return !(
      v.x > d.w ||
      v.y > d.h ||
      v.x + v.w < 0 ||
      v.y + v.h < 0
    )
  }
  getPorts(v) {
    const d = this.d
    const p = d.mapId[v.pid]

    if (!p) return []

    const x1 = d.isH ? p.x + p.w / 2 : (d.isL ? p.x + p.w : p.x)
    const y1 = d.isH ? (d.isT ? p.y + p.h : p.y) : p.y + p.h / 2

    const x4 = d.isH ? v.x + v.w / 2 : (!d.isL ? v.x + v.w : v.x)
    const y4 = d.isH ? (!d.isT ? v.y + v.h : v.y) :  v.y + v.h / 2

    const x2 = d.isH ? x1 : (x1 + x4) / 2
    const y2 = d.isH ? (y1 + y4) / 2 : y1

    const x3 = d.isH ? x4 : (x1 + x4) / 2
    const y3 = d.isH ? (y1 + y4) / 2 : y4

    return [
      x1, y1,
      x2, y2,
      x3, y3,
      x4, y4,
    ]
  }
  renderLine() {
    const d = this.d
    const gd = d.canvas.getContext('2d')

    const lineJoin = v => {
      const p = d.mapId[v.pid]
      if (
        !p ||
        !(
          this.isNodeInScene(p) ||
          this.isNodeInScene(v) ||
          !(
            p.x > d.w && v.x > d.w ||
            p.y > d.h && v.y > d.h ||
            p.x < 0 && v.x < 0 ||
            p.y < 0 && v.y < 0
          )
        )
      ) return

      const [
        x1, y1,
        x2, y2,
        x3, y3,
        x4, y4,
      ] = this.getPorts(v)

      gd.moveTo(x1, y1)
      gd.bezierCurveTo(
        x2, y2,
        x3, y3,
        x4, y4,
      )
    }

    gd.beginPath()
    d.stair.forEach((row) => {
      row.forEach(v => {
        lineJoin(v)
      })
    })

    gd.lineWidth = d.conf.lineWidth
    gd.strokeStyle = d.color.grey
    gd.stroke()

    if (d.isHighlight) {
      gd.beginPath()
      
      let t = d.curNode
      while (t) {
        lineJoin(t)
        t = d.mapId[t.pid]
      }

      gd.lineWidth = d.conf.lineWidth + 1
      gd.strokeStyle = d.color.highlight
      gd.stroke()
    }
  }
  renderNode() {
    const d = this.d
    const gd = d.canvas.getContext('2d')
    const drawNode = v => {
      if (!this.isNodeInScene(v)) return

      gd.beginPath()
      gd.rect(v.x + 1, v.y + 1, v.w - 2, v.h - 2)
      gd.fillStyle = (
        mapHighlight[v.id] ? d.color.highlight : (
          v === d.curNode ? d.color.curNode : (
            d.mapSelected[v.id] ? d.color.selected : (
              v.fillStyle || d.color.grey
            )
          )
        )
      )
      gd.fill()

      gd.font = d.conf.font
      gd.textAlign = 'left'
      gd.textBaseline = 'middle'
      gd.fillStyle = '#fff'
      v.name_.forEach((str, idx, arr) => {
        gd.fillText(
          str,
          v.x + d.conf.cellPaddingH,
          v.y + v.h / 2 + (idx - arr.length / 2 + .5) * d.conf.lineHeight
        )
      })

      if (d.mapPid[v.id]?.length > 0) {
        const r = 8
        const r2 = r / 2
        let [x, y] = this.getPorts(d.mapPid[v.id][0])

        switch (d.direction) {
          case 'l': x += r / 2 + r2; break
          case 't': y += r / 2 + r2; break
          case 'r': x -= r / 2 + r2; break
          case 'b': y -= r / 2 + r2; break
        }

        gd.beginPath()
        gd.arc(x, y, r, 0, 2 * Math.PI)
        gd.fillStyle = 'rgba(255, 255, 255, .9)'
        gd.fill()
        gd.strokeStyle = 'rgba(128, 128, 128, 1)'
        gd.lineWidth = 1
        gd.stroke()

        gd.beginPath()
        gd.moveTo(x - 4, y)
        gd.lineTo(x + 4, y)

        if (d.mapClose[v.id]) {
          gd.moveTo(x, y - 4)
          gd.lineTo(x, y + 4)
        }

        gd.stroke()
      }
    }

    const forward = []
    const mapHighlight = {}
    const visited = {}
    const dfs = v => {
      if (visited[v.id])
        return
      visited[v.id] = true
      forward.push(v)
      this.getChildren(v).forEach(dfs)
    }

    if (d.isHighlight) {
      const nodes = this.getHighlightNodes()
      nodes.forEach(v => mapHighlight[v.id] = v)
      nodes.length > 0 && dfs(nodes[0])
    } else if (d.curNode) {
      dfs(d.curNode)
    } else {
      Object.values(d.mapSelected).forEach(dfs)
    }

    this.each(v => !visited[v.id] && drawNode(v))

    for (let i = forward.length - 1; i > -1; i--) {
      drawNode(forward[i])
    }
  }
  renderRec() {
    const d = this.d
    const gd = d.canvas.getContext('2d')
    const {x, y, w, h} = d.rec

    if (!w && !h) return

    gd.beginPath()
    gd.rect(x + .5, y + .5, w, h)
    gd.fillStyle = d.rec.bgColor || 'rgba(0, 170, 255, .5)'
    gd.strokeStyle = d.rec.borderColor || 'rgba(0, 170, 255, 1)'
    gd.lineWidth = 1
    gd.fill()
    gd.stroke()
  }
  render() {
    const d = this.d
    const canvas = d.canvas
    const gd = canvas.getContext('2d')

    gd.clearRect(0, 0, d.w * d.conf.scale, d.h * d.conf.scale)
    gd.save()
    gd.scale(d.conf.scale, d.conf.scale)
    this.renderLine()
    this.renderNode()
    this.renderRec()
    gd.restore()
  }
}
